var Rappid = Backbone.Router.extend({

    routes: {
        '*path': 'home'
    },

    home: function() {

        this.initializeEditor();
    },

    initializeEditor: function() {

        this.initializePaper();
        this.initializeStencil();
        this.initializeSelection();
        this.initializeHaloAndInspector();
        this.initializeClipboard();
        this.initializeCommandManager();
        this.initializeToolbar();
        // Intentionally commented out. See the `initializeValidator()` method for reasons.
        // Uncomment for demo purposes.
        this.initializeValidator();
        // Commented out by default. You need to run `node channelHub.js` in order to make
        // channels working. See the documentation to the joint.com.Channel plugin for details.
        // this.initializeChannel();
    },

    // Create a grapfh, paper and wrap the paper in a PaperScroller.
    initializePaper: function() {
        
        this.graph = new joint.dia.Graph;

        this.paperScroller = new joint.ui.PaperScroller({ autoResizePaper: true });

        this.paper = new joint.dia.Paper({
            el: this.paperScroller.el,
            width: 1000,
            height: 1000,
            gridSize: 10,
            perpendicularLinks: true,
            model: this.graph,
            defaultLink: new joint.dia.Link({
                attrs: {// lien continu noir
                    // @TODO: scale(0) fails in Firefox
                    '.marker-source': { d: 'M 10 0 L 0 5 L 10 10 z', transform: 'scale(0.001)' },
                    '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' },
                    '.connection': {
                        // filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2 } }
                    }
                }
            }),
            defaultLink1: new joint.dia.Link({
                attrs: {//lien pointillé noir

                    // @TODO: scale(0) fails in Firefox
                    '.marker-source': { d: 'M 10 0 L 0 5 L 10 10 z', transform: 'scale(0.001)' },
                    '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' },

                    '.connection': {
                        'stroke-dasharray':'10,5'

                         //filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2, rx : '5,10' } },

                    }
                }
            }),
            defaultLink2: new joint.dia.Link({
                attrs: {//lien continu gris

                    // @TODO: scale(0) fails in Firefox
                    '.marker-source': { d: 'M 10 0 L 0 5 L 10 10 z', transform: 'scale(0.001)' },
                    '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z', 'fill':'gray' },

                    '.connection': {
                        'stroke':'gray'
                        //filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2, rx : '5,10' } },

                    }
                }
            }),
            defaultLink3: new joint.dia.Link({
                attrs: {//avec cercle au target
                    // @TODO: scale(0) fails in Firefox
                    '.marker-source': { d: 'M 10 0 L 0 5 L 10 10 z', transform: 'scale(0.001)' },
                    '.marker-target': { d:'m 466.868,179.07401 c 0,1.93189 -1.56611,3.498 -3.498,3.498 -1.9319,0 -3.498,-1.56611 -3.498,-3.498 0,-1.9319 1.5661,-3.498 3.498,-3.498 1.93189,0 3.498,1.5661 3.498,3.498 z',cx:'3000' ,cy:'1000', r:'1000', fill:'none', width:'100',transform: 'scale(2)','stroke':'gray'},

                    '.connection': {
                        'stroke':'gray'
                        //filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2, rx : '5,10' } },

                    }
                }
            })


    });
        this.paperScroller.options.paper = this.paper;

        $('.paper-container').append(this.paperScroller.render().el);

        this.paperScroller.center();

        this.graph.on('add', this.initializeLinkTooltips, this);
    },

    initializeLinkTooltips: function(cell) {

        if (cell instanceof joint.dia.Link) {

            var linkView = this.paper.findViewByModel(cell);
            new joint.ui.Tooltip({
                className: 'tooltip small',
                target: linkView.$('.tool-options'),
                content: 'Click to open Inspector for this link',
                left: linkView.$('.tool-options'),
                direction: 'left'
            });

            
        }
    },

    // Create and popoulate stencil.
    initializeStencil: function() {

        this.stencil = new joint.ui.Stencil({ graph: this.graph, paper: this.paper, width: 240, groups: Stencil.groups });
        $('.stencil-container').append(this.stencil.render().el);

        _.each(Stencil.groups, function(group, name) {
            
            this.stencil.load(Stencil.shapes[name], name);
            joint.layout.GridLayout.layout(this.stencil.getGraph(name), {
                columnWidth: this.stencil.options.width / 2 - 10,
                columns: 2,
                rowHeight: 80,
                resizeToFit: true,
                dy: 10,
                dx: 10
            });
            this.stencil.getPaper(name).fitToContent(1, 1, 10);

        }, this);

        $('.stencil-container .btn-expand').on('click', _.bind(this.stencil.openGroups, this.stencil));
        $('.stencil-container .btn-collapse').on('click', _.bind(this.stencil.closeGroups, this.stencil));

        this.initializeStencilTooltips();
    },

    initializeStencilTooltips: function() {

        // Create tooltips for all the shapes in stencil.
        _.each(this.stencil.graphs, function(graph) {

            graph.get('cells').each(function(cell) {

                new joint.ui.Tooltip({
                    target: '.stencil [model-id="' + cell.id + '"]',
                    content: cell.get('type').split('.')[1],
                    left: '.stencil',
                    direction: 'left'
                });
            });
        });
    },

    initializeSelection: function() {
        
        this.selection = new Backbone.Collection;
        this.selectionView = new joint.ui.SelectionView({ paper: this.paper, graph: this.graph, model: this.selection });

        // Initiate selecting when the user grabs the blank area of the paper while the Shift key is pressed.
        // Otherwise, initiate paper pan.
        this.paper.on('blank:pointerdown', function(evt, x, y) {

            if (_.contains(KeyboardJS.activeKeys(), 'shift')) {
                this.selectionView.startSelecting(evt, x, y);
            } else {
                this.paperScroller.startPanning(evt, x, y);
            }
        }, this);

        this.paper.on('cell:pointerdown', function(cellView, evt) {
            // Select an element if CTRL/Meta key is pressed while the element is clicked.
            if ((evt.ctrlKey || evt.metaKey) && !(cellView.model instanceof joint.dia.Link)) {
                this.selectionView.createSelectionBox(cellView);
                this.selection.add(cellView.model);
            }
        }, this);

        this.selectionView.on('selection-box:pointerdown', function(evt) {
            // Unselect an element if the CTRL/Meta key is pressed while a selected element is clicked.
            if (evt.ctrlKey || evt.metaKey) {
                var cell = this.selection.get($(evt.target).data('model'));
                this.selectionView.destroySelectionBox(this.paper.findViewByModel(cell));
                this.selection.reset(this.selection.without(cell));
            }
        }, this);

        // Disable context menu inside the paper.
        // This prevents from context menu being shown when selecting individual elements with Ctrl in OS X.
        this.paper.el.oncontextmenu = function(evt) { evt.preventDefault(); };

        KeyboardJS.on('delete, backspace', _.bind(function() {
            
            this.commandManager.initBatchCommand();
            this.selection.invoke('remove');
            this.commandManager.storeBatchCommand();
            this.selectionView.cancelSelection();
        }, this));
    },

    createInspector: function(cellView) {

        // No need to re-render inspector if the cellView didn't change.
        if (!this.inspector || this.inspector.options.cellView !== cellView) {
            
            if (this.inspector) {
                // Clean up the old inspector if there was one.
                this.inspector.remove();
            }

            var inspectorDefs = InspectorDefs[cellView.model.get('type')];

            this.inspector = new joint.ui.Inspector({
                inputs: inspectorDefs ? inspectorDefs.inputs : CommonInspectorInputs,
                groups: inspectorDefs ? inspectorDefs.groups : CommonInspectorGroups,
                cellView: cellView
            });

            this.initializeInspectorTooltips();
            
            this.inspector.render();
            $('.inspector-container').html(this.inspector.el);
        }
    },

    initializeInspectorTooltips: function() {
        
        this.inspector.on('render', function() {

            this.inspector.$('[data-tooltip]').each(function() {

                var $label = $(this);
                new joint.ui.Tooltip({
                    target: $label,
                    content: $label.data('tooltip'),
                    right: '.inspector',
                    direction: 'right'
                });
            });
            
        }, this);
    },

    initializeHaloAndInspector: function() {

        this.paper.on('cell:pointerup', function(cellView, evt) {

            if (cellView.model instanceof joint.dia.Link || this.selection.contains(cellView.model)) return;

            // In order to display halo link magnets on top of the freetransform div we have to create the
            // freetransform first. This is necessary for IE9+ where pointer-events don't work and we wouldn't
            // be able to access magnets hidden behind the div.
            var freetransform = new joint.ui.FreeTransform({ graph: this.graph, paper: this.paper, cell: cellView.model });
            var halo = new joint.ui.Halo({ graph: this.graph, paper: this.paper, cellView: cellView });

            freetransform.render();
            halo.render();

            this.initializeHaloTooltips(halo);

            this.createInspector(cellView);

            this.selection.reset([cellView.model]);
            
        }, this);

        this.paper.on('link:options', function(evt, cellView, x, y) {

            this.createInspector(cellView);
        }, this);
    },

    initializeHaloTooltips: function(halo) {

        new joint.ui.Tooltip({
            className: 'tooltip small',
            target: halo.$('.remove'),
            content: 'Click to remove the object',
            direction: 'right',
            right: halo.$('.remove'),
            padding: 15
        });

        new joint.ui.Tooltip({
            className: 'tooltip small',
            target: halo.$('.fork'),
            content: 'Lien en trait continu gris',
            direction: 'left',
            left: halo.$('.fork'),
            padding: 15
        });
        new joint.ui.Tooltip({
            className: 'tooltip small',
            target: halo.$('.clone'),
            content: 'Lien avec cercle au target',
            direction: 'left',
            left: halo.$('.clone'),
            padding: 15
        });
        new joint.ui.Tooltip({
            className: 'tooltip small',
            target: halo.$('.unlink'),
            content: 'Click to break all connections to other objects',
            direction: 'right',
            right: halo.$('.unlink'),
            padding: 15
        });
        new joint.ui.Tooltip({
            className: 'tooltip small',
            target: halo.$('.link'),
            content: 'Click and drag to connect the object',
            direction: 'left',
            left: halo.$('.link'),
            padding: 15
        });

        new joint.ui.Tooltip({
            className: 'tooltip small',
            target: halo.$('.rotate'),
            content: 'lien pointillé',
            direction: 'right',
            right: halo.$('.rotate'),
            padding: 15
        });


    },

    initializeClipboard: function() {

        this.clipboard = new joint.ui.Clipboard;
        
        KeyboardJS.on('ctrl + c', _.bind(function() {
            // Copy all selected elements and their associated links.
            this.clipboard.copyElements(this.selection, this.graph, { translate: { dx: 20, dy: 20 }, useLocalStorage: true });
        }, this));
        
        KeyboardJS.on('ctrl + v', _.bind(function() {
            this.clipboard.pasteCells(this.graph);
            this.selectionView.cancelSelection();

            this.clipboard.pasteCells(this.graph, { link: { z: -1 }, useLocalStorage: true });

            // Make sure pasted elements get selected immediately. This makes the UX better as
            // the user can immediately manipulate the pasted elements.
            var selectionTmp = [];
            
            this.clipboard.each(function(cell) {

                if (cell.get('type') === 'link') return;

                // Push to the selection not to the model from the clipboard but put the model into the graph.
                // Note that they are different models. There is no views associated with the models
                // in clipboard.
                selectionTmp.push(this.graph.getCell(cell.id));
                this.selectionView.createSelectionBox(this.paper.findViewByModel(cell));
            }, this);

            this.selection.reset(selectionTmp);
        }, this));

        KeyboardJS.on('ctrl + x', _.bind(function() {

            var originalCells = this.clipboard.copyElements(this.selection, this.graph, { useLocalStorage: true });
            this.commandManager.initBatchCommand();
            _.invoke(originalCells, 'remove');
            this.commandManager.storeBatchCommand();
            this.selectionView.cancelSelection();
        }, this));
    },

    initializeCommandManager: function() {

        this.commandManager = new joint.dia.CommandManager({ graph: this.graph });

        KeyboardJS.on('ctrl + z', _.bind(function() {

            this.commandManager.undo();
            this.selectionView.cancelSelection();
        }, this));
        
        KeyboardJS.on('ctrl + y', _.bind(function() {

            this.commandManager.redo();
            this.selectionView.cancelSelection();
        }, this));
    },


    initializeValidator: function() {
        var graph=this.graph;
        var i=0;
        //var commandManager = new joint.dia.CommandManager({ graph: this.graph });
        this.validator = new joint.dia.Validator({ commandManager: this.commandManager });

        this.validator.validate('change:source change:target add ',

            function(err, command, next) {
                if (command.data.type === 'link'){
                    var link = command.data.attr || graph.getCell(command.data.id).toJSON()
                    var sourceId = link.source.id;
                    var targetId = link.target.id;
                // settings example
                var loops = false; //link loops allowed / denied
                var paperPin = true; //allow link to pin to paper
                var type = 'type'; // attribute name in the cell the rules working with
                var rules = { // allowed cells (value) for certain cell type (key)
                    'devs.Atomic':{},
                    'basic.AndroidTextView':{},
                    'basic.AndroidEditText':{},
                    'basic.AndroidButton':{},
                    'basic.AndroidCheckBox':{},
                    'basic.AndroidImageView':{},
                    'basic.AndroidImageView2':{},
                    'basic.AndroidRadioButton':{},
                    'basic.AndroidToggleButton':{},
                    'basic.ImageUpEvent':{},
                    'basic.Out':{},
                    'uml.Class':{},
                    'basic.start':{allow: ['basic.AndroidTextView','basic.AndroidEditText','basic.AndroidButton','basic.AndroidCheckBox','basic.AndroidImageView','basic.AndroidImageView2','basic.AndroidRadioButton','basic.AndroidToggleButton','basic.ImageUpEvent','basic.Out','uml.Class']}
                };

                // The cell in Parameter "command" is meant to be a link.
                // see clear function

                // source and target are both cells
                if (sourceId && targetId) {

                    // deny loops if settings don't allow them
                    if (!loops && sourceId === targetId) {
                        return next('Loop are not allowed');
                    }

                    //find source and target type attribute (parameter "type" in settings)
                    var sourceType =  graph.getCell(sourceId).get(type), targetType =  graph.getCell(targetId).get(type);

                    var cellRules = rules[sourceType];
                    if (cellRules && (cellRules.allow || cellRules.deny)) {

                        var isInRuleArray = _.contains(cellRules[cellRules.allow ? 'allow' : 'deny'], targetType);

                        if ((cellRules.allow && !isInRuleArray) || (cellRules.deny && isInRuleArray)) {
                            return next('Wrong type of element');
                        } else {

                            return next(err);

                        }

                    } else {

                        // no rules exists, anything is valid
                        return next(err);
                    }
                }

                if (paperPin) return next(err);

                return next('Linking elements only');
                }


            },
            function multiplicity(err, command, next) {
                var link = command.data.attr || graph.getCell(command.data.id).toJSON();

                var sourceId = link.source.id;
                var targetId = link.target.id;

                // settings example
                var loops = true; // link loops allowed / denied
                var paperPin = false;  //allow link to pin to paper
                var multiLinks = false; // allow existence of 2 or more links with some target and source
                var type = 'type'; // attribute name in the cell the rules working with
                var rules = { // allowed cells (value) for certain cell type (key)
                    'basic.start': {
                         //"*" any number of links + ignore multiLinks
                        'basic.AndroidTextView': 1,
                        'basic.AndroidEditText':1,
                        'basic.AndroidButton':1,
                        'basic.AndroidCheckBox':1,
                        'basic.AndroidImageView':1,
                        'basic.AndroidImageView2':1,
                        'basic.AndroidRadioButton':1,
                        'basic.AndroidToggleButton':1,
                        'basic.Out':1},

                    'basic.AndroidTextView':{'basic.ImageUpEvent':1},
                    'basic.AndroidEditText':{'basic.ImageUpEvent':1},
                    'basic.AndroidButton':{'basic.ImageUpEvent':1},
                    'basic.AndroidCheckBox':{'basic.ImageUpEvent':1},
                    'basic.AndroidImageView':{'basic.ImageUpEvent':1},
                    'basic.AndroidImageView2':{'basic.ImageUpEvent':1},
                    'basic.AndroidRadioButton':{'basic.ImageUpEvent':1},
                    'basic.AndroidToggleButton':{'basic.ImageUpEvent':1},
                    'basic.Out':{'basic.ImageUpEvent':1},
                    'devs.Atomic':{'uml.Class':1},
                    'uml.Class':{}
                };

                // see clear function


                // if source nor target isn't a cell, check settings whether it allows loops
                if (!sourceId || !targetId) {
                    return paperPin ? next(err) : next("Links can't be pinned to the paper");
                }

                // deny loops if settings don't allow them
                if (!loops && sourceId === targetId) {
                    return next('Loop are not allowed');
                }

                var sourceCell = graph.getCell(sourceId);
                var sourceType = sourceCell.get(type);
                var targetType = graph.getCell(targetId).get(type);

                var rulesBySource = rules[sourceType];

                //if (!rulesBySource) return next(err);

                 var arity = rulesBySource[targetType];
                if (arity === '*') return next(err); // arity * means no limits for number of links
                if(sourceType==='basic.start'){
                if (arity === 0 || i=== 1) {return next(sourceType + " can't have any links.")};}
                else{
                    if (arity === 0) {return next(sourceType + " can't have any links.")};
                }

                // get all outboundlinks except the one kept in parameter command
                var outboundLinks = _.reject(graph.getConnectedLinks(sourceCell, { outbound : true  }), function(lnk) {
                    return lnk.id === link.id;
                });

                // how many links with certain target type the source cell already have
                // return error if any other link has same soure and target and multiLinks are not allowed in settings
                var numberLinks = 0;
                if (_.find(outboundLinks, function(lnk) {
                    numberLinks += (graph.getCell(lnk.get('target').id).get(type) === targetType) ? 1 : 0;
                    return (!multiLinks && lnk.get('target').id === targetId);
                }, this)) return next("Link from " + sourceType + " to " + targetType + " already exists.");

                if (numberLinks >= arity) return next(sourceType + " can't have more than " + arity + " links");
                if(sourceType=== 'basic.start'){
                    i=1;
                }
                return next(err);
            },

        function(err, command, next) {
                if (err) console.log(err);
                return next(err);
            }
        );
        this.validator.validate('remove ',

            function(err, command, next) {
                if (command.data.type === 'link'){
                    var link = command.data.attributes;
                    var sourceId = link.source.id;
                    var sourceCell = graph.getCell(sourceId);
                    var sourceType = sourceCell.attributes.type;
                    if(sourceType=== 'basic.start'){
                        i=0;
                    }
                }

            });

        this.validator.validate('add change:position',

            function(err, command, next) {

               if(command.data.type==='link'){return next(err);} else {
                var t = command.data.attributes || graph.getCell(command.data.id).toJSON()
                var type='type';
                var area = g.rect(t.position.x, t.position.y, t.size.width, t.size.height);
                   if (command.data.type === 'basic.start'){
                       if(document.getElementsByClassName("element basic start").length > 3){
                           return next('pas plus que 1');
                       }
                       console.log(document.getElementsByClassName("element basic start").length);
                   }
                if (_.find(graph.getElements(), function (e) {

                    var position = e.get('position');
                    var size = e.get('size');
                    var fils=graph.getCell(t.id);
                    var parent=graph.getCell(e.id);
                    var parentType=parent.get(type);

                    if (e.id !== t.id && area.intersect(g.rect(position.x, position.y, size.width, size.height))){
                        if(parentType==='devs.Atomic'){

                        e.embed(fils);
                        console.log(e.getEmbeddedCells().length);
                        return(false);

                    }



                    }

                   // return(false);

                }))
                    return next("Another cell in the way!");
                else

                    return next(err);

            }},
        function(err, command, next) {
            if (err) console.log(err);
            return next(err);
        }
        );
        this.validator.validate('add change:position',
        function(err,command,next){

            if (command.data.type === 'basic.start'){
                if(document.getElementsByClassName("element basic start").length > 3){
                    return next('pas plus que 1');
                }
            console.log(document.getElementsByClassName("element basic start").length);
            }
            },
            function(err, command, next) {
                if (err) console.log(err);
                return next(err);
            }



        );

        this.validator.validate('change:position',
            function(err,command,next){
                var t = command.data.attributes || graph.getCell(command.data.id).toJSON()
                var element=graph.getCell(t.id);

            },
            function(err, command, next) {
                if (err) console.log(err);
                return next(err);
            }



        );
        graph.on('change:position', function(cell, newPosition, opt) {

            if (opt.skipParentHandler) return;

            if (cell.get('embeds') && cell.get('embeds').length) {
                // If we're manipulating a parent element, let's store
                // it's original position to a special property so that
                // we can shrink the parent element back while manipulating
                // its children.
                cell.set('originalPosition', cell.get('position'));
            }

            var parentId = cell.get('parent');
            if (!parentId) return;

            var parent = graph.getCell(parentId);
            var parentBbox = parent.getBBox();

            if (!parent.get('originalPosition')) parent.set('originalPosition', parent.get('position'));
            if (!parent.get('originalSize')) parent.set('originalSize', parent.get('size'));

            var originalPosition = parent.get('originalPosition');
            var originalSize = parent.get('originalSize');

            var newX = originalPosition.x;
            var newY = originalPosition.y;
            var newCornerX = originalPosition.x + originalSize.width;
            var newCornerY = originalPosition.y + originalSize.height;

            _.each(parent.getEmbeddedCells(), function(child) {

                var childBbox = child.getBBox();

                if (childBbox.x < newX) { newX = childBbox.x; }
                if (childBbox.y < newY) { newY = childBbox.y; }
                if (childBbox.corner().x > newCornerX) { newCornerX = childBbox.corner().x; }
                if (childBbox.corner().y > newCornerY) { newCornerY = childBbox.corner().y; }
            });

            // Note that we also pass a flag so that we know we shouldn't adjust the
            // `originalPosition` and `originalSize` in our handlers as a reaction
            // on the following `set()` call.
            parent.set({
                position: { x: newX, y: newY },
                size: { width: newCornerX - newX, height: newCornerY - newY }
            }, { skipParentHandler: true });
        });



    },

    initializeToolbar: function() {

        this.initializeToolbarTooltips();
        
        $('#btn-undo').on('click', _.bind(this.commandManager.undo, this.commandManager));
        $('#btn-redo').on('click', _.bind(this.commandManager.redo, this.commandManager));
        $('#btn-clear').on('click', _.bind(this.graph.clear, this.graph));
        $('#btn-svg').on('click', _.bind(this.paper.openAsSVG, this.paper));
        $('#btn-png').on('click', _.bind(this.openAsPNG, this));
        $('#btn-zoom-in').on('click', _.bind(this.zoomIn, this));
        $('#btn-zoom-out').on('click', _.bind(this.zoomOut, this));
        $('#btn-fullscreen').on('click', _.bind(this.toggleFullscreen, this));
        $('#btn-print').on('click', _.bind(this.paper.print, this.paper));

        // toFront/toBack must be registered on mousedown. SelectionView empties the selection
        // on document mouseup which happens before the click event. @TODO fix SelectionView?
        $('#btn-to-front').on('mousedown', _.bind(function(evt) { this.selection.invoke('toFront'); }, this));
        $('#btn-to-back').on('mousedown', _.bind(function(evt) { this.selection.invoke('toBack'); }, this));

        $('#btn-layout').on('click', _.bind(this.layoutDirectedGraph, this));
        
        $('#input-gridsize').on('change', _.bind(function(evt) {
            var gridSize = parseInt(evt.target.value, 10);
            $('#output-gridsize').text(gridSize);
            this.setGrid(gridSize);
        }, this));
    },

    initializeToolbarTooltips: function() {
        
        $('.toolbar-container [data-tooltip]').each(function() {
            
            new joint.ui.Tooltip({
                target: $(this),
                content: $(this).data('tooltip'),
                top: '.toolbar-container',
                direction: 'top'
            });
        });
    },

    openAsPNG: function() {
        
        var windowFeatures = 'menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes';
        var windowName = _.uniqueId('png_output');
        var imageWindow = window.open('', windowName, windowFeatures);

        this.paper.toPNG(function(dataURL) {
	    imageWindow.document.write('<img src="' + dataURL + '"/>');
        }, { padding: 10 });
    },

    zoom: function(newZoomLevel, ox, oy) {

        if (_.isUndefined(this.zoomLevel)) { this.zoomLevel = 1; }

        if (newZoomLevel > 0.2 && newZoomLevel < 20) {

	    ox = ox || (this.paper.el.scrollLeft + this.paper.el.clientWidth / 2) / this.zoomLevel;
	    oy = oy || (this.paper.el.scrollTop + this.paper.el.clientHeight / 2) / this.zoomLevel;

	    this.paper.scale(newZoomLevel, newZoomLevel, ox, oy);

	    this.zoomLevel = newZoomLevel;
        }
    },

    zoomOut: function() { this.zoom(this.zoomLevel - 0.2); },
    zoomIn: function() { this.zoom(this.zoomLevel + 0.2); },

    toggleFullscreen: function() {

        var el = document.body;

        function prefixedResult(el, prop) {
            
            var prefixes = ['webkit', 'moz', 'ms', 'o', ''];
            for (var i = 0; i < prefixes.length; i++) {
                var prefix = prefixes[i];
                var propName = prefix ? (prefix + prop) : (prop.substr(0, 1).toLowerCase() + prop.substr(1));
                if (!_.isUndefined(el[propName])) {
                    return _.isFunction(el[propName]) ? el[propName]() : el[propName];
                }
            }
        }

        if (prefixedResult(document, 'FullScreen') || prefixedResult(document, 'IsFullScreen')) {
            prefixedResult(document, 'CancelFullScreen');
        } else {
            prefixedResult(el, 'RequestFullScreen');
        }
    },

    setGrid: function(gridSize) {

        this.paper.options.gridSize = gridSize;
        
        var backgroundImage = this.getGridBackgroundImage(gridSize);
        $(this.paper.svg).css('background-image', 'url("' + backgroundImage + '")');
    },

    getGridBackgroundImage: function(gridSize, color) {

        var canvas = $('<canvas/>', { width: gridSize, height: gridSize });

        canvas[0].width = gridSize;
        canvas[0].height = gridSize;

        var context = canvas[0].getContext('2d');
        context.beginPath();
        context.rect(1, 1, 1, 1);
        context.fillStyle = color || '#AAAAAA';
        context.fill();

        return canvas[0].toDataURL('image/png');
    },

    layoutDirectedGraph: function() {

        this.commandManager.initBatchCommand();
        
        _.each(this.graph.getLinks(), function(link) {

            // Reset vertices.
            link.set('vertices', []);
            
            // Remove all the non-connected links.
            if (!link.get('source').id || !link.get('target').id) {
                link.remove();
            }
        });
        
        joint.layout.DirectedGraph.layout(this.graph, { setLinkVertices: false, rankDir: 'LR', rankDir: 'TB' });

        // Scroll to the top-left corner as this is the initial position for the DirectedGraph layout.
        this.paperScroller.el.scrollLeft = 0;
        this.paperScroller.el.scrollTop = 0;
        
        this.commandManager.storeBatchCommand();
    },

    initializeChannel: function(url) {

        var room = (location.hash && location.hash.substr(1)) || joint.util.uuid();
        this.navigate('#' + room);
        var channel = this.channel = new joint.com.Channel({ graph: this.graph, url: url || 'ws://localhost:4141', query: { room: room } });

        var roomUrl = location.href.replace(location.hash, '') + '#' + room;
        $('.statusbar-container').html('Send this link to a friend to collaborate in real-time: <a href="' + roomUrl + '" target="_blank">' + roomUrl + '</a>');
    }
});

// Main.
// -----

var app = new Rappid;
Backbone.history.start();
